/*
 * Copyright (C) 2005 the xine-project
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 *
 * $Id: xml_widgets.c,v 1.25 2006/04/04 21:56:18 dsalt Exp $
 *
 * windows, generated from XML and using JS and gxine's .gtkrc
 */

#include "globals.h"

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <pthread.h>

#include <gdk/gdk.h>
#include <gtk/gtk.h>
#include <xine/xmlparser.h>

#include "xml_widgets.h"
#include "drag_drop.h"
#include "gtkflipbutton.h"
#include "ui.h"
#include "utils.h"
#include "engine.h"

#define TN_(txt) (txt)

#ifdef MAKE_TRANSLATION_SOURCE
# define TRANSLATE(txt) (txt)

static void
comment (const xml_node_t *node, char *text)
{
  char *lf;

  g_print (_("\n// node type: %s\n"), node->name);
  if (!text)
    return;

  while ((lf = strchr (text, '\n')) != NULL)
  {
    *lf = 0;
    printf ("// %s\n", text);
    text = lf + 1;
  }
  if (*text)
    printf ("// %s\n", text);
}

static void
translatable (const char *text)
{
  char *lf;
  if (!text || !*text)
    return;

  assert (*text == '~');
  if (!*++text)
    return;

  printf ("TN_(\"");
  while ((lf = strchr (text, '"')) != NULL)
  {
    *lf = 0;
    printf ("%s\\\"", text);
    text = lf + 1;
  }
  printf ("%s\");\n", text);
}

#define xml_error(msg, ...) g_printerr ((msg), ## __VA_ARGS__)

#else
# define TRANSLATE(txt) my_gettext (txt)

struct widget_ref {
  GtkWidget *object;
  const char *id;
};

static pthread_mutex_t xml_lock = PTHREAD_MUTEX_INITIALIZER;
static se_o_t *se_widget_space = NULL;
static GSList *mnemonic_list, *widget_id_list;
static char *xml_domain;

static const char *
my_gettext (const char *src)
{
  if (!src)
    return "";
  if (*src != '~')
    return src;
  src = dgettext (xml_domain ? : PACKAGE".theme", src + 1);
  return src + (*src == '~');
}

static gboolean
check_stock (const char *stock)
{
  GtkStockItem item;
  return gtk_stock_lookup (stock, &item)
	 || gtk_icon_factory_lookup_default (stock);
}

/* yes, this is intended to be annoying */
#define xml_error(msg, ...) \
  display_error_modal (FROM_GXINE, _("XML error"), (msg), ##__VA_ARGS__)

#endif

/* Callbacks */

#ifndef MAKE_TRANSLATION_SOURCE
static void
destroy_notify (gpointer data)
{
  free (data);
}

static gboolean
button_js_cb (GtkWidget *w, const char *data)
{
  if (data && !no_recursion)
    engine_exec (data + *data + 2, NULL, NULL, data + 1);
  return TRUE;
}

static gboolean
button_toggled_cb (GtkToggleButton *w, gpointer data)
{
  return
    button_js_cb
      (NULL, g_object_get_data (G_OBJECT (w),
				w->active ? "js-activate" : "js-deactivate"));
}

static void
stock_control_cb (GtkWidget *w, gpointer data)
{
  if (no_recursion)
    return;
  engine_exec (data, NULL, NULL, NULL);
  /* avoid problems with the space key (key bindings) */
  gtk_window_set_focus (GTK_WINDOW (gtk_widget_get_toplevel (w)), NULL);
}

static void
stock_toggle_cb (GtkToggleButton *button, gpointer data)
{
  if (!no_recursion)
    ui_set_status (UI_CURRENT_STATE);
}

#define JS_WIDGET() ((se_o_t *)JS_GetPrivate (cx, obj))->user_data

static JSBool
js_set_show (JSContext *cx, JSObject *obj,
	     uintN argc, jsval *argv, jsval *rval)
{
  int v, all = 0;
  se_log_fncall ("<widget>.set_show");
  se_argc_check_range (1, 2, "<widget>.set_show");
  se_arg_is_int_or_bool (0, "<widget>.set_show");
  JS_ValueToBoolean(cx, argv[0], &v);
  if (argc == 2)
    JS_ValueToBoolean(cx, argv[0], &all);
  if (all)
  {
    if (v)
      gtk_widget_show_all (JS_WIDGET ());
    else
      gtk_widget_hide_all (JS_WIDGET ());
  }
  else if (v)
    gtk_widget_show (JS_WIDGET ());
  else
    gtk_widget_hide (JS_WIDGET ());
  return JS_TRUE;
}

static JSBool
js_set_sensitive (JSContext *cx, JSObject *obj,
		  uintN argc, jsval *argv, jsval *rval)
{
  int v;
  se_log_fncall ("<widget>.set_sensitive");
  se_argc_check (1, "<widget>.set_sensitive");
  se_arg_is_int_or_bool (0, "<widget>.set_sensitive");
  JS_ValueToBoolean (cx, argv[0], &v);
  gtk_widget_set_sensitive (JS_WIDGET (), v);
  return JS_TRUE;
}

static JSBool
js_activate (JSContext *cx, JSObject *obj,
	     uintN argc, jsval *argv, jsval *rval)
{
  se_log_fncall ("<widget>.activate");
  gtk_widget_activate (JS_WIDGET ());
  return JS_TRUE;
}

static gboolean
js_listen_cb (gpointer widget)
{
  engine_exec (g_object_get_data (widget, "js-listen"),
	       NULL, NULL, _("XML widget listener"));
  return FALSE;
}

static int
js_listen (void *widget, se_t *cx, se_o_t *obj, se_prop_t *prop,
	   se_prop_read_t r)
{
  g_idle_add (js_listen_cb, widget);
  return 0;
}
#endif

/* Misc */

#ifndef MAKE_TRANSLATION_SOURCE
static GtkTooltips *
get_tips_object (void)
{
  static GtkTooltips *tips = NULL;
  if (!tips)
  {
    tips = gtk_tooltips_new ();
    gtk_tooltips_enable (tips);
  }
  return tips;
}

#define xml_parser_get_bool(node,prop) \
  xml_parser_get_property_int ((node), (prop), FALSE)

static void
command_attach (GtkWidget *w, const char *event, const char *label,
		const char *cmd, const char *what)
{
  GObject *object = G_OBJECT (w);
  char *data = g_strdup_printf ("%c%s%c%s", (int) strlen (what), what, 0, cmd);
  g_object_set_data_full (object, label, data, destroy_notify);
  if (event)
    g_signal_connect (object, event, G_CALLBACK (button_js_cb), data);
}
#endif

/* XML parser */

static GtkWidget *widget_parse (xml_node_t *);

#ifndef MAKE_TRANSLATION_SOURCE
static int
lookup_type (xml_node_t *node, const char *attr, const char *const labels[])
{
  const char *arg = xml_parser_get_property (node, attr) ? : "";
  int type = -1;
  while (labels[++type])
    if (!strcasecmp (arg, labels[type]))
      break;
  return type;
}

static int
lookup_shadow_type (xml_node_t *node)
{
  static const char *const labels[] = {
    "etched-out", "etched-in", "out", "in", NULL
  };
  return G_N_ELEMENTS (labels) - 1 - lookup_type (node, "shadow", labels);
}

static int
lookup_image_size (xml_node_t *node)
{
  static const char *const labels[] = {
    "menu", "small-toolbar", "large-toolbar", "dnd", "drag-drop",
    "dialog", "dialogue", NULL
  };
  static const guchar map[G_N_ELEMENTS (labels)] = {
    GTK_ICON_SIZE_MENU, GTK_ICON_SIZE_SMALL_TOOLBAR,
    GTK_ICON_SIZE_LARGE_TOOLBAR, GTK_ICON_SIZE_DND, GTK_ICON_SIZE_DND,
    GTK_ICON_SIZE_DIALOG, GTK_ICON_SIZE_DIALOG,
    GTK_ICON_SIZE_BUTTON /* default */
  };

  return map[lookup_type (node, "size", labels)];
}
#endif

#ifndef MAKE_TRANSLATION_SOURCE
static int
parse_attach (xml_node_t *node, const char *attr, int dflt)
{
  int value = 0;
  const char *attach = xml_parser_get_property (node, attr);
  if (!attach)
    return dflt;
  while (*attach)
  {
    int or = 0;
    if (!strncasecmp (attach, "expand", 6))
    {
      or = GTK_EXPAND;
      attach += 6;
    }
    else if (!strncasecmp (attach, "fill", 4))
    {
      or = GTK_FILL;
      attach += 4;
    }
    else if (!strncasecmp (attach, "shrink", 6))
    {
      or = GTK_SHRINK;
      attach += 6;
    }
    if (*attach && *attach++ != ',')
      break;
    value |= or;
  }
  return value;
}
#endif

/* Common properties:
 *	NAME		TYPE	DEFAULT	COMMENT
 *	name		string	varies	for gtkrc styling
 *	width		int	varies	widget's request width
 *	height		int	varies	widget's request height
 *	show		string	none	command(s) on widget being auto-shown
 *	hide		string	none	command(s) on widget being auto-hidden
 */

/* Declarations */
static GtkWidget *create_box (xml_node_t *, gboolean);


/*
 * Interactive widgets
 */

static GtkWidget *
create_button (xml_node_t *node)
/* Properties:
 *	stock		string	none		stock item
 *	label		string	none		text if stock is not set [translatable]
 *	image		string	none		stock image if stock is not set
 *	alt		string	none		alternate text [translatable]
 *	relief		string	"normal"	(or "half" or "none")
 *	mnemonic	boolean	false		underline usage
 *	onclick		string	none		Javascript, executed on click
 *
 * If none of stock, label, image are used, child items are expected.
 * You get an automatic centred hbox to contain them.
 */
{
  char *label, *image, *alt;
#ifndef MAKE_TRANSLATION_SOURCE
  GtkWidget *button;
  static const char *relief[] = { "none", "half", NULL };
#endif

  alt = xml_parser_get_property (node, "alt");

  if ((image = xml_parser_get_property (node, "stock")) != NULL)
  {
#ifdef MAKE_TRANSLATION_SOURCE
    if (alt && *alt == '~')
    {
      comment (node, xml_parser_get_property (node, "onclick") ? : image);
      translatable (alt);
    }
    return NULL;
#else
    if (check_stock (image))
      button = gtk_button_new_from_stock (image);
    else if (alt)
      button = xml_parser_get_bool (node, "mnemonic")
	       ? gtk_button_new_with_mnemonic (TRANSLATE (alt))
	       : gtk_button_new_with_label (TRANSLATE (alt));
    else
    {
      button = gtk_button_new ();
      if (image)
	g_object_set (G_OBJECT (button), "image",
		      check_stock (image)
		      ? gtk_image_new_from_stock (image, GTK_ICON_SIZE_BUTTON)
		      : gtk_image_new_from_icon_name (image, GTK_ICON_SIZE_BUTTON),
		      NULL);
      else
	g_object_set (G_OBJECT (button), "image",
		gtk_image_new_from_stock (GTK_STOCK_MISSING_IMAGE,
					  GTK_ICON_SIZE_BUTTON),
		NULL);
    }

    goto created;
#endif
  }

  image = xml_parser_get_property (node, "image");
  label = xml_parser_get_property (node, "label");
#ifdef MAKE_TRANSLATION_SOURCE
  if (label && *label == '~')
  {
    comment (node, xml_parser_get_property (node, "onclick"));
    translatable (label);
  }
  else if (image && alt && *alt == '~')
  {
    comment (node, xml_parser_get_property (node, "onclick") ? : image);
    translatable (alt);
  }
  else if (!label && !image) /* other content */
    create_box (node, FALSE);
  return NULL;
#else

  if (label && !image)
  {
    button = xml_parser_get_bool (node, "mnemonic")
	     ? gtk_button_new_with_mnemonic (TRANSLATE (label))
	     : gtk_button_new_with_label (TRANSLATE (label));
    goto created;
  }

  button = gtk_button_new ();
  if (label || image)
  {
    g_object_set (G_OBJECT (button),
	"label", TRANSLATE (label),
	"use-underline", xml_parser_get_bool (node, "mnemonic"),
	NULL);

    if (image && check_stock (image))
      /* image stock item exists */
      g_object_set (G_OBJECT (button),
	"image", gtk_image_new_from_stock (image, GTK_ICON_SIZE_BUTTON),
	NULL);
    else if (!label && alt)
      /* image stock item doesn't exist but we have alt text */
      g_object_set (G_OBJECT (button),
	"label", TRANSLATE (alt),
	NULL);
    else if (!label && image)
      /* no alt text; try themed icon name */
      g_object_set (G_OBJECT (button),
	"image", gtk_image_new_from_icon_name (image, GTK_ICON_SIZE_BUTTON),
	NULL);
    else if (!label)
      g_object_set (G_OBJECT (button),
	"image", gtk_image_new_from_stock (GTK_STOCK_MISSING_IMAGE,
					   GTK_ICON_SIZE_BUTTON),
	NULL);
  }
  else
  {
    /* no label, no image: must be other content */
    GtkWidget *align = gtk_alignment_new (0.5, 0.5, 0, 0);
    gtk_container_add (GTK_CONTAINER (button), align);
    /* create & attach the child element (implied hbox) */
    gtk_container_add (GTK_CONTAINER (align), create_box (node, FALSE));
  }

  created:
  gtk_button_set_relief (GTK_BUTTON (button),
			 2 - lookup_type (node, "relief", relief));
  /* set the command which is executed when the button is clicked */
  label = xml_parser_get_property (node, "onclick");
  if (label)
    command_attach (button, "clicked", "js-onclick", label,
		    _("XML button click"));
  return button;
#endif /* ! MAKE_TRANSLATION_SOURCE */
}


/*
static GtkWidget *
create_flip_button (xml_node_t *node)
*/
/* Properties:
 *	activate	string	none	Javascript, executed on click (->on)
 *	deactivate	string	none	Javascript, executed on click (->off)
 * Contains exactly two children.
 */
/*
{
  GtkWidget *button, *off = NULL, *on = NULL;
  char *cmd;

  if (node->child)
  {
    off = node->child ? widget_parse (node->child) : NULL;
    if (node->child->next)
      on = widget_parse (node->child);
  }
  button = gtk_flip_button_new (off, on);

  if ((cmd = xml_parser_get_property (node, "activate")) != NULL)
    g_object_set_data_full (G_OBJECT (button), "js-activate", g_strdup (cmd),
			    destroy_notify);
  if ((cmd = xml_parser_get_property (node, "activate")) != NULL)
    g_object_set_data_full (G_OBJECT (button), "js-deactivate", g_strdup (cmd),
			    destroy_notify);
  g_signal_connect (G_OBJECT (button), "toggled",
		    G_CALLBACK (button_toggled_cb), NULL);

  return button;
}
*/

#ifndef MAKE_TRANSLATION_SOURCE
typedef enum { BUTTON = 1, FLIP, SLIDER, SPIN } stock_e;
typedef struct {
  stock_e type;
  int control;
  const char *name, *tip, *id1, *id2, *cmd1, *cmd2;
} stock_t;

static GtkWidget *
stock_button_common (GtkWidget *button, int control)
{
  ui_register_control_button (control, button);
  gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
  gtk_widget_show_all (button);
  return button;
}

static void
set_tip (GtkWidget *w, const stock_t *stock)
{
  if (stock->tip)
    gtk_tooltips_set_tip (get_tips_object (), w,
			  TRANSLATE (*stock->tip ? stock->tip : stock->name),
			  NULL);
}
#endif

static GtkWidget *
create_stock_gxine (xml_node_t *node)
/* Properties:
 *	type		string	none	widget identifer (see stock[].name)
 *	vertical	bool	FALSE	slider orientation
 *	show-value	bool	FALSE	show slider value numerically
 */
{
#ifndef MAKE_TRANSLATION_SOURCE
  stock_t stock[] = {
    { BUTTON,	Control_PLAY,		"play",		TN_("Start or resume a stream\n(at normal speed)"),
      GTK_STOCK_MEDIA_PLAY, NULL,			"play ();" },
    { BUTTON,	Control_FASTFWD,	"forward",	TN_("Fast forward"),
      GTK_STOCK_MEDIA_FORWARD, NULL,			"if (!is_live_stream ()) av_speed.v = 5;" },
    { BUTTON,	Control_PAUSE,		"pause",	TN_("Pause"),
      GTK_STOCK_MEDIA_PAUSE, NULL,			"if (!is_live_stream ()) pause ();" },
    { BUTTON,	Control_STOP,		"soft-stop",	TN_("Stop"),
      GTK_STOCK_MEDIA_STOP, NULL,			"vdr ('STOP') || stop ();" },
    { BUTTON,	Control_STOP,		"stop",		TN_("Stop stream playback"),
      GTK_STOCK_MEDIA_STOP, NULL,			"stop ();" },
    { FLIP,	Control_MUTE,		"mute",		TN_("Mute/unmute"),
      GXINE_MEDIA_SPEAKER_MUTE, GXINE_MEDIA_SPEAKER,	"if (!vdr ('MUTE')) ao_mute.v = false;", "if (!vdr ('MUTE')) ao_mute.v = true;" },
    { SLIDER,	Control_SEEKER,		"stream",	TN_("Stream position") },
    { SPIN,	Control_AUDIO_CHANNEL,	TN_("Audio channel"), "" },
    { SLIDER,	Control_COMPRESSOR,	TN_("Compressor"), "" },
    { SLIDER,	Control_AMPLIFIER,	TN_("Amplifier"), "" },
    { SLIDER,	Control_AV_SYNC,	TN_("A/V sync"), "" },
    { SLIDER,	Control_HUE,		TN_("Hue"), "" },
    { SLIDER,	Control_SATURATION,	TN_("Saturation"), "" },
    { SLIDER,	Control_CONTRAST,	TN_("Contrast"), "" },
    { SLIDER,	Control_BRIGHTNESS,	TN_("Brightness"), "" },
    { SLIDER,	Control_VOLUME,		TN_("Volume"), "" },
    { SLIDER,	Control_EQ_30,		"eq-30",	TN_("Graphic equaliser, 30Hz") },
    { SLIDER,	Control_EQ_60,		"eq-60",	TN_("Graphic equaliser, 60Hz") },
    { SLIDER,	Control_EQ_125,		"eq-125",	TN_("Graphic equaliser, 125Hz") },
    { SLIDER,	Control_EQ_250,		"eq-250",	TN_("Graphic equaliser, 250Hz") },
    { SLIDER,	Control_EQ_500,		"eq-500",	TN_("Graphic equaliser, 500Hz") },
    { SLIDER,	Control_EQ_1K,		"eq-1k",	TN_("Graphic equaliser, 1kHz") },
    { SLIDER,	Control_EQ_2K,		"eq-2k",	TN_("Graphic equaliser, 2kHz") },
    { SLIDER,	Control_EQ_4K,		"eq-4k",	TN_("Graphic equaliser, 4kHz") },
    { SLIDER,	Control_EQ_8K,		"eq-8k",	TN_("Graphic equaliser, 8kHz") },
    { SLIDER,	Control_EQ_16K,		"eq-16k",	TN_("Graphic equaliser, 16kHz") },
    {}
  };
  static const char *const spintypes[] = { "slider", "hslider", "vslider", "spin", NULL };

  GtkWidget *w;
  int i;
  const char *id = xml_parser_get_property (node, "type");

  if (!id)
    return NULL;

  for (i = 0; stock[i].type; ++i)
  {
    enum { CTRL_SLIDER, CTRL_SLIDER_H, CTRL_SLIDER_V, CTRL_SPIN, CTRL_ANY } spin, spec;

    if (strcasecmp (id, stock[i].name))
      continue;
    switch (stock[i].type)
    {
    case BUTTON:
      w = check_stock (stock[i].id1)
	  ? ui_toggle_button_new_stock (stock[i].id1)
	  : ui_toggle_button_new_icon_name (stock[i].id1);
      g_object_connect (G_OBJECT (w),
	"signal::clicked", G_CALLBACK (stock_control_cb), (char *)stock[i].cmd1,
	"signal::toggled", G_CALLBACK (stock_toggle_cb), NULL,
	NULL);
      set_tip (w, &stock[i]);
      return stock_button_common (w, stock[i].control);

    case FLIP:
      w = check_stock (stock[i].id1)
	  ? gtk_flip_button_new_from_stock (stock[i].id1, stock[i].id2,
					    GTK_ICON_SIZE_BUTTON)
	  : gtk_flip_button_new_from_icon_names (stock[i].id1, stock[i].id2,
						 GTK_ICON_SIZE_BUTTON);
      command_attach (w, NULL, "js-activate", stock[i].cmd1, _("XML button click"));
      command_attach (w, NULL, "js-deactivate", stock[i].cmd2, _("XML button click"));
      g_signal_connect (G_OBJECT (w), "toggled",
			G_CALLBACK (button_toggled_cb), NULL);
      set_tip (w, &stock[i]);
      return stock_button_common (w, stock[i].control);

    case SLIDER:
      spin = CTRL_SLIDER;
      goto do_spin_slider;

    case SPIN:
      spin = CTRL_SPIN;

      do_spin_slider:
      spec = (typeof (spec)) lookup_type (node, "mode", spintypes);

      do_spin_slider_again:
      switch (spec)
      {
      case CTRL_SLIDER:
	w = xml_parser_get_bool (node, "vertical")
	    ? gtk_vscale_new (GTK_ADJUSTMENT (ui_register_control_adjustment
						(stock[i].control)))
	    : gtk_hscale_new (GTK_ADJUSTMENT (ui_register_control_adjustment
						(stock[i].control)));
	break;
      case CTRL_SLIDER_H:
	w = gtk_hscale_new (GTK_ADJUSTMENT (ui_register_control_adjustment
					      (stock[i].control)));
	break;
      case CTRL_SLIDER_V:
	w = gtk_vscale_new (GTK_ADJUSTMENT (ui_register_control_adjustment
					      (stock[i].control)));
	break;
      case CTRL_SPIN:
	w = gtk_spin_button_new
	      (GTK_ADJUSTMENT (ui_register_control_adjustment
				 (stock[i].control)),
	     1.0, 0);
        break;
      default:
	spec = spin;
	goto do_spin_slider_again;
      }

      if (GTK_IS_SCALE (w))
      {
	gtk_scale_set_draw_value
	  (GTK_SCALE (w), xml_parser_get_bool (node, "show-value"));
	gtk_range_set_inverted
	  (GTK_RANGE (w), xml_parser_get_bool (node, "inverted"));
      }
      else
	gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (w), TRUE);

      set_tip (w, &stock[i]);
      return w;

    default:
      return NULL;
    }
  }
  g_printerr (_("xml_widgets: unrecognised stock control id '%s'\n"), id);
#endif
  return NULL;
}


/*
 * Display widgtets
 */

static GtkWidget *
create_arrow (xml_node_t *node)
/* Properties:
 *	type		string	"right"	arrow direction
 *	shadow		string	"none"	arrow border
 */
{
#ifdef MAKE_TRANSLATION_SOURCE
  return NULL;
#else
  static const char *const arrownames[] = { "up", "down", "left", NULL };
  return gtk_arrow_new (lookup_type (node, "type", arrownames),
			lookup_shadow_type (node));
#endif
}

static GtkWidget *
create_image (xml_node_t *node)
/* Properties:
 *	stock		string	"gtk-stock-missing-image"
 *	size		string	"button"
 */
{
#ifdef MAKE_TRANSLATION_SOURCE
  const char *alt = xml_parser_get_property (node, "alt");
  if (alt && *alt == '~')
  {
    comment (node, xml_parser_get_property (node, "onclick") ? : xml_parser_get_property (node, "stock"));
    translatable (alt);
  }
  return NULL;
#else
  const char *item = xml_parser_get_property (node, "stock");
  if (item && check_stock (item))
    return gtk_image_new_from_stock (item, lookup_image_size (node));
  const char *alt = xml_parser_get_property (node, "alt");
  if (alt)
    return gtk_label_new (TRANSLATE (alt));
  if (item)
    return gtk_image_new_from_icon_name (item, lookup_image_size (node));
  return gtk_image_new_from_stock (GTK_STOCK_MISSING_IMAGE,
				   lookup_image_size (node));
#endif
}

static GtkWidget *
create_label (xml_node_t *node)
/* Properties:
 *	text		string	""
 *	markup		bool	FALSE
 *	mnemonic	string	NULL	reference to another widget (via id)
 *					implies underline usage
 *	width-chars	int	none
 *	wrap		bool	FALSE
 *	... - fixme
 */
{
#ifdef MAKE_TRANSLATION_SOURCE
  char *text = xml_parser_get_property (node, "text");
  if (text && *text == '~')
    translatable (text);
  return NULL;
#else
  char *mnemonic = xml_parser_get_property (node, "mnemonic");
  GtkWidget *label = gtk_label_new
		       (TRANSLATE (xml_parser_get_property (node, "text")));
  g_object_set (G_OBJECT (label),
	"use-markup", xml_parser_get_bool (node, "markup"),
	"use-underline", mnemonic && *mnemonic,
	"width-chars", xml_parser_get_property_int (node, "width-chars", -1),
	"wrap", xml_parser_get_bool (node, "wrap"),
	NULL);
  if (mnemonic && *mnemonic)
  {
    struct widget_ref *ref = malloc (sizeof (*ref));
    ref->object = label;
    ref->id = mnemonic;
    mnemonic_list = g_slist_append (mnemonic_list, ref);
  }
  return label;
#endif
}


/*
 * Container widgets
 */

static GtkWidget *
create_box (xml_node_t *node, gboolean vertical)
/* Properties:
 *	homogeneous	bool	false
 *	spacing		int	0	spacing between items
 *	expand		bool	false	child property
 *	fill		bool	false	child property
 *	padding	int	2	child property
 * Child properties:
 *	box-expand	bool	=expand
 *	box-fill	bool	=fill
 *	box-padding	int	=padding
 *
 * May contain <end /> to mark where packing is switched from start
 */
{
#ifndef MAKE_TRANSLATION_SOURCE
  gboolean homogeneous = xml_parser_get_bool (node, "homogeneous");
  gboolean expand = xml_parser_get_bool (node, "expand");
  gboolean fill = xml_parser_get_bool (node, "fill");
  int padding = xml_parser_get_property_int (node, "spacing", 0);

  GtkWidget *box = vertical
		 ? gtk_vbox_new (homogeneous, padding)
		 : gtk_hbox_new (homogeneous, padding);
  typeof (gtk_box_pack_start) *func = gtk_box_pack_start;

  padding = xml_parser_get_property_int (node, "padding", 2);
#endif

  foreach_glist (node, node->child)
    if (strcasecmp (node->name, "end"))
#ifdef MAKE_TRANSLATION_SOURCE
      widget_parse (node);
  return NULL;
#else
      func (GTK_BOX (box), widget_parse (node),
	    xml_parser_get_property_int (node, "box-expand", expand),
	    xml_parser_get_property_int (node, "box-fill", fill),
	    xml_parser_get_property_int (node, "box-padding", padding));
    else
      func = gtk_box_pack_end;
  return box;
#endif
}

static GtkWidget *
create_table (xml_node_t *node)
/* <table> properties:
 *	rows		int	1		range 1..100
 *	cols		int	1		range 1..100
 *	homogeneous	bool	FALSE
 *	x-padding	int	0		cell property
 *	y-padding	int	=x-padding	cell property
 *	x-attach	string	expand,fill	cell property
 *	y-attach	string	=x-attach	cell property
 * The padding and attachment options can be overridden in <tr> and <td>.
 *
 * <tr>: child of <table>, has no other properties
 *
 * <td>: child of <tr>
 * Properties:
 *	colspan		int	1
 *	rowspan		int	1
 *
 * Cell content which would lie beyond the specified table limits will NOT be
 * created. You do *not* get an automatic box in each cell.
 */
{
#define GET_X_PAD(NODE,v) xml_parser_get_property_int ((NODE), "x-padding", (v))
#define GET_Y_PAD(NODE,d,v) \
  (strcasecmp (xml_parser_get_property ((NODE), "y-padding") ? : "", "x") \
   ? (d) : xml_parser_get_property_int ((NODE), "y-padding", (v)))
#define GET_X_ATTACH(NODE,v) parse_attach ((NODE), "x-attach", (v))
#define GET_Y_ATTACH(NODE,d,v) \
  (strcasecmp (xml_parser_get_property ((NODE), "y-attach") ? : "", "x") \
   ? (d) : parse_attach ((NODE), "y-attach", (v)))

  int rows = xml_parser_get_property_int (node, "rows", 1);
  int cols = xml_parser_get_property_int (node, "cols", 1);
#ifndef MAKE_TRANSLATION_SOURCE
  int xpad = GET_X_PAD (node, 0);
  int ypad = GET_Y_PAD (node, -1, xpad);
  int xatt = GET_X_ATTACH (node, GTK_EXPAND | GTK_FILL);
  int yatt = GET_Y_ATTACH (node, -1, xatt);
#endif
  if (rows < 1 || rows > 100 || cols < 1 || cols > 100)
    g_printerr (_("warning: table rows, cols range is 1 to 100 - clipping\n"));

  rows = MIN (100, MAX (1, rows));
  cols = MIN (100, MAX (1, cols));

  {
#ifndef MAKE_TRANSLATION_SOURCE
    GtkWidget *table =
      gtk_table_new (rows, cols, xml_parser_get_bool (node, "homogeneous"));
#endif
    gboolean occupied[rows][cols];
    int row = 0;
    xml_node_t *rownode;

    memset (occupied, 0, rows * cols * sizeof (gboolean));

    foreach_glist (rownode, node->child)
    {
      if (!strcasecmp (rownode->name, "tr"))
      {
	xml_node_t *cellnode;
#ifndef MAKE_TRANSLATION_SOURCE
	int rxpad = GET_X_PAD (rownode, xpad);
	int rypad = GET_Y_PAD (rownode, rxpad, ypad);
	int rxatt = GET_X_ATTACH (rownode, xatt);
	int ryatt = GET_Y_ATTACH (rownode, rxatt, yatt);
#endif
	int col = 0;
	foreach_glist (cellnode, rownode->child)
	{
	  while (col < cols && occupied[row][col])
	    ++col;
	  if (col == cols)
	    break;

	  if (!strcasecmp (cellnode->name, "td"))
	  {
	    int r, c;
#ifndef MAKE_TRANSLATION_SOURCE
	    int xa, ya;
#endif
	    int width = xml_parser_get_property_int (node, "colspan", 1);
	    int height = xml_parser_get_property_int (node, "rowspan", 1);
	    height = MIN (MAX (1, height), rows - row);
	    width = MIN (MAX (1, width), cols - col);
	    /* mark cells as occupied */
	    for (r = row; r < rows && r < row + height; ++r)
	      for (c = col; c < cols && c < col + width; ++c)
		occupied[r][c] = TRUE;
	    /* create & attach the child element (implied hbox) */
#ifdef MAKE_TRANSLATION_SOURCE
	    widget_parse (cellnode->child);
#else
	    c = GET_X_PAD (cellnode, rxpad);
	    r = GET_Y_PAD (cellnode, c, rypad);
	    xa = GET_X_ATTACH (cellnode, rxatt);
	    ya = GET_Y_ATTACH (cellnode, xa, ryatt);
	    gtk_table_attach (GTK_TABLE (table),
			      widget_parse (cellnode->child),
			      col, col + width, row, row + height,
			      xa, ya == -1 ? xa : ya,
			      c, r == -1 ? c : r);
#endif
	    if (++col == cols)
	      break;
	  }
        }
	if (++row == rows)
	  break;
      }
    }

#ifndef MAKE_TRANSLATION_SOURCE
    row = xml_parser_get_property_int (node, "spacing", 0);
    g_object_set (G_OBJECT (table),
		  "column-spacing",
		  xml_parser_get_property_int (node, "column-spacing", row),
		  "row-spacing",
		  xml_parser_get_property_int (node, "row-spacing", row),
		  NULL);

    return table;
#endif
  }
  return NULL;
}


/*
 * Top-level stuff
 */

static GtkWidget *
widget_parse (xml_node_t *node)
{
  GtkWidget *widget = NULL;
  switch (tolower (node->name[0]))
  {
  case 'a':
    if (!strcasecmp (node->name + 1, "rrow"))
      widget = create_arrow (node);
    break;

  case 'b':
    if (!strcasecmp (node->name + 1, "utton"))
      widget = create_button (node);
    break;

  case 'c':
    if (!strcasecmp (node->name + 1, "ontrol"))
      widget = create_stock_gxine (node);
    break;

/*
  case 'f':
    if (!strcasecmp (node->name + 1, "lipbutton"))
      widget = create_flip_button (node);
    break;
*/

  case 'h':
    if (!strcasecmp (node->name + 1, "box"))
      widget = create_box (node, FALSE);
#ifndef MAKE_TRANSLATION_SOURCE
    else if (!strcasecmp (node->name + 1, "separator"))
      widget = gtk_hseparator_new ();
#endif
    break;

  case 'i':
    if (!strcasecmp (node->name + 1, "mage"))
      widget = create_image (node);
    break;

  case 'l':
    if (!strcasecmp (node->name + 1, "abel"))
      widget = create_label (node);
    break;

  case 't':
    if (!strcasecmp (node->name + 1, "able"))
      widget = create_table (node);
#ifndef MAKE_TRANSLATION_SOURCE
    else if (!strcasecmp (node->name + 1, "ime"))
    {
      widget = create_time_widget (xml_parser_get_bool (node, "small"));
      timewidgets = g_slist_append (timewidgets, widget);
    }
    else if (!strcasecmp (node->name + 1, "itle"))
    {
      widget = create_infobar (xml_parser_get_bool (node, "small"));
      infobars = g_slist_append (infobars, widget);
    }
#endif
    break;

  case 'v':
    if (!strcasecmp (node->name + 1, "box"))
      widget = create_box (node, TRUE);
#ifndef MAKE_TRANSLATION_SOURCE
    else if (!strcasecmp (node->name + 1, "separator"))
      widget = gtk_vseparator_new ();
#endif
    break;
  }

#ifdef MAKE_TRANSLATION_SOURCE
  {
    char *text = xml_parser_get_property (node, "tip");
    if (text && *text == '~')
    {
      comment (node, xml_parser_get_property (node, "onclick"));
      translatable (text);
    }
  }
#else
  if (widget)
  {
    char *name;

    gtk_widget_set_size_request
      (widget, xml_parser_get_property_int (node, "width", -1),
       xml_parser_get_property_int (node, "height", -1));

    if (xml_parser_get_bool (node, "accept-drop"))
      drag_drop_setup (widget,
		       xml_parser_get_property_int (node, "autoplay", TRUE));

    name = xml_parser_get_property (node, "name");
    if (name && name[0])
      gtk_widget_set_name (widget, name);

    name = xml_parser_get_property (node, "tip");
    if (name)
      gtk_tooltips_set_tip (get_tips_object (), widget, TRANSLATE (name), NULL);

    name = xml_parser_get_property (node, "id");
    if (name && *name)
    {
      se_o_t *se_widget;
      char *se_name, *se_dot;
      struct widget_ref *ref;
      static const se_f_def_t defs[] = {
	{ "set_show", js_set_show, 0, 0, SE_GROUP_HIDDEN, NULL, NULL },
	{ "set_sensitive", js_set_sensitive, 0, 0, SE_GROUP_HIDDEN, NULL, NULL },
	{ "activate", js_activate, 0, 0, SE_GROUP_HIDDEN, NULL, NULL },
	{ NULL }
      };
      if (!se_widget_space)
	se_widget_space = se_create_object (gse, NULL, "widget", NULL,
					    SE_GROUP_HIDDEN, NULL);
      se_widget = se_widget_space;
      se_dot = se_name = name = g_strdup (name);
      for (; se_name;)
      {
	se_dot = strchr (se_dot, '.');
	if (se_dot)
	  *se_dot++ = 0;
	se_widget = se_find_object (gse, se_widget, se_name)
		    ? : se_create_object (gse, se_widget, se_name, widget,
					  SE_GROUP_HIDDEN, NULL);
	if (!se_widget)
	{
	  g_printerr (_("xml_widgets: failed to create JS object 'widget.%s'\n"),
		   name);
	  break;
	}
	se_name = se_dot;
      }
      free (name);
      if (se_widget)
	se_defuns (gse, se_widget, defs);
      else
        return widget; /* no point in continuing... */

      ref = malloc (sizeof (*ref));
      ref->object = widget;
      ref->id = name;
      widget_id_list = g_slist_append (widget_id_list, ref);
    }

    name = xml_parser_get_property (node, "onshow");
    if (name)
      command_attach (widget, "show", "js-onshow", name, _("XML widget show"));

    name = xml_parser_get_property (node, "onhide");
    if (name)
      command_attach (widget, "hide", "js-onhide", name, _("XML widget hide"));

    name = xml_parser_get_property (node, "listen");
    if (name)
    {
      const char *cmd = xml_parser_get_property (node, "onchange");
      if (cmd)
      {
	g_object_set_data_full (G_OBJECT (widget), "js-listen", g_strdup (cmd),
				destroy_notify);
	se_prop_add_listener (gse, se_get_object (gse, NULL, name), "v",
			      js_listen, G_OBJECT (widget));
      }
    }
  }
#endif

  return widget;
}

static GtkWidget *
widget_create_from_xml_internal (const char *file, const char *name,
				 const char *title, gboolean window,
				 gboolean warn_missing)
{
  GtkWidget *widget = NULL;
  GError *error = NULL;
  xml_node_t *tree, *node;
  char *xml_data;
#ifndef MAKE_TRANSLATION_SOURCE
  GSList *mnemonic;

  mnemonic_list = widget_id_list = NULL;
  xml_domain = NULL;
#endif

  g_file_get_contents (file, &xml_data, NULL, &error);
  if (!xml_data)
  {
    /* we don't always want to report "not found" errors */
    if (warn_missing || error->domain != G_FILE_ERROR ||
	error->code != G_FILE_ERROR_NOENT)
      xml_error ("xml_widgets: %s\n", error->message);
    g_clear_error (&error);
    return NULL;
  }

  xml_parser_init (xml_data, strlen (xml_data), XML_PARSER_CASE_INSENSITIVE);
  if (xml_parser_build_tree (&tree) < 0)
  {
    xml_error (_("xml_widgets: error in %s: XML parser failed\n"), file);
    free (xml_data);
    return NULL;
  }

  node = tree;
  if (!strcasecmp (node->name, "gettext"))
  {
#ifndef MAKE_TRANSLATION_SOURCE
    xml_domain = xml_parser_get_property (node, "domain");
    if (xml_domain && *xml_domain)
    {
      char *slash = strrchr (file, '/');
      /* assert (slash && slash > file); */
      *slash = 0;
      xml_domain = g_strconcat (PACKAGE".theme.", xml_domain, NULL);
      bindtextdomain (xml_domain, file);
      bind_textdomain_codeset (xml_domain, "UTF-8");
      *slash = '/';
    }
    else
      xml_domain = NULL;
#endif
    node = node->next;
  }

  if (!strcasecmp (node->name, "window"))
  {
#ifdef MAKE_TRANSLATION_SOURCE
    widget_parse (node->child);
#else
    if (!window)
      g_printerr (_("xml_widgets: sorry, <window> is not allowed in %s\n"), file);
    else
    {
      widget = gtk_window_new (GTK_WINDOW_TOPLEVEL);
      if (name)
	gtk_widget_set_name (widget, name);
      gtk_window_set_title (GTK_WINDOW (widget), title);
      gtk_container_add (GTK_CONTAINER (widget), widget_parse (node->child));
      gtk_window_set_resizable (GTK_WINDOW (widget), FALSE);
      g_signal_connect (G_OBJECT (widget), "delete-event",
		        G_CALLBACK (gtk_widget_hide_on_delete), NULL);
    }
#endif
  }
#ifndef MAKE_TRANSLATION_SOURCE
  else if (window)
    g_printerr (_("xml_widgets: sorry, <window> is required in %s\n"), file);
#endif
  else
    widget = widget_parse (node);

  xml_parser_free_tree (tree);

#ifndef MAKE_TRANSLATION_SOURCE
  foreach_glist (mnemonic, mnemonic_list)
  {
    struct widget_ref *ref = mnemonic->data;
    GSList *widget_id;
    foreach_glist (widget_id, widget_id_list)
    {
      struct widget_ref *w = widget_id->data;
      if (strcasecmp (w->id, ref->id))
	gtk_label_set_mnemonic_widget (GTK_LABEL (ref->object), w->object);
    }
    free (ref);
  }

  /* free globals */
  g_slist_foreach (widget_id_list, (GFunc) free, NULL);
  g_slist_free (mnemonic_list);
  g_slist_free (widget_id_list);
  free (xml_domain);

#endif

  /* free locals */
  free (xml_data);

  return widget;
}

#ifndef MAKE_TRANSLATION_SOURCE
GtkWidget *
widget_create_from_xml (const char *leaf, const char *name, const char *title,
			gboolean window, gboolean warn_missing)
{
  GtkWidget *widget = NULL;
  char *file;

  /* there is global data here :-) */
  pthread_mutex_lock (&xml_lock);

  file = get_config_filename (leaf);
  widget = widget_create_from_xml_internal (file, name, title, window, FALSE);
  if (!widget)
  {
    free (file);
    file = g_build_filename (confdir, leaf, NULL);
    widget = widget_create_from_xml_internal (file, name, title, window,
					      warn_missing);
  }
  free (file);

  /* no re-entrancy issues left... */
  pthread_mutex_unlock (&xml_lock);

  return widget;
}
#endif

#ifdef MAKE_TRANSLATION_SOURCE
int
main (int argc, char *argv[])
{
  if (argc != 2)
  {
    g_printerr (_("Source file?\n"));
    return 1;
  }

  widget_create_from_xml_internal (argv[1], NULL, NULL, FALSE, TRUE);
}
#endif
